package com.wg.db.sql;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Entity;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.wg.db.exception.DataException;
import com.wg.db.FormatTimeUnit;
import com.wg.db.schema.SchemaColumn;
import com.wg.db.schema.SchemaTable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 数据库操作工具类<br>
 * 提供基于表结构元数据的通用增删改查功能，支持实体类与数据库表的映射转换。
 *
 * @author 少爷123
 */
public class SqlRepository {
    private static final Log log = LogFactory.get(SqlRepository.class);

    // ========================== 表创建 ==========================

    /**
     * 创建当前表及下月表（按时间格式分区）<br>
     * 仅当时间单位非 NONE 时生效
     *
     * @param schemaTable 表结构元数据（需包含时间格式定义）
     * @throws DataException 若表创建失败时抛出
     */
    public static void createTable(SchemaTable schemaTable) {
        Objects.requireNonNull(schemaTable, "表结构元数据不能为 null");

        if (schemaTable.getTimeUnit() == FormatTimeUnit.NONE) {
            log.debug("跳过表创建：时间单位为 NONE，表名={}", schemaTable.getRealTableName());
            return;
        }

        try {
            String currentTableName = schemaTable.getTableNameByFormat(0);
            String nextTableName = schemaTable.getTableNameByFormat(1);
            String baseModelTable = schemaTable.getModelTableName();

            // 生成建表 SQL（使用 LIKE 复制模型表结构）
            String createCurrentSql = StrUtil.format("CREATE TABLE IF NOT EXISTS `{}` LIKE `{}`;",
                    currentTableName, baseModelTable);
            String createNextSql = StrUtil.format("CREATE TABLE IF NOT EXISTS `{}` LIKE `{}`;",
                    nextTableName, baseModelTable);

            // 批量执行建表语句
            schemaTable.getDb().executeBatch(createCurrentSql, createNextSql);
            log.info("表创建成功：当前表={}, 下月表={}", currentTableName, nextTableName);
        } catch (Exception e) {
            String errorMsg = StrUtil.format("表创建失败：表名={}，错误信息={}",
                    schemaTable.getRealTableName(), e.getMessage());
            log.error(errorMsg, e);
            throw new DataException(errorMsg, e);
        }
    }

    // ========================== 单条记录查询 ==========================

    /**
     * 根据主键查询单条记录（自动映射到实体类）
     *
     * @param klass       实体类类型（需包含无参构造函数）
     * @param schemaTable 表结构元数据（需包含主键定义）
     * @param entityId    主键值（需与主键字段类型兼容）
     * @param <T>         实体类型
     * @return 实体对象（未找到时返回 null）
     * @throws DataException 若查询失败或实体类无效时抛出
     */
    public static <T> T get(Class<T> klass, SchemaTable schemaTable, Object entityId) {
        // 参数校验
        Objects.requireNonNull(klass, "实体类不能为 null");
        Objects.requireNonNull(schemaTable, "表结构元数据不能为 null");
        Objects.requireNonNull(entityId, "主键值不能为 null");

        String sql = MySqlExpert.genSelectSql(schemaTable);
        try {
            List<Entity> results = schemaTable.getDb().query(sql, entityId);
            if (results.isEmpty()) {
                log.debug("未找到记录：表={}，主键值={}", schemaTable.getRealTableName(), entityId);
                return null;
            }

            return mapFirstEntityToEntity(klass, schemaTable, results.get(0));
        } catch (Exception e) {
            String errorMsg = StrUtil.format("查询单条记录失败：表={}，主键值={}，错误信息={}",
                    schemaTable.getRealTableName(), entityId, e.getMessage());
            log.error(errorMsg, e);
            throw new DataException(errorMsg, e);
        }
    }

    // ========================== 全量记录查询 ==========================

    /**
     * 查询表中所有记录（自动映射到实体类）
     *
     * @param klass       实体类类型（需包含无参构造函数）
     * @param schemaTable 表结构元数据（需包含列定义）
     * @param <T>         实体类型
     * @return 实体列表（无记录时返回空列表）
     * @throws DataException 若查询失败或实体类无效时抛出
     */
    public static <T> List<T> getAll(Class<T> klass, SchemaTable schemaTable) {
        return getAll(klass, schemaTable, MySqlExpert.genSelectAllSql(schemaTable)); // 复用带 SQL 参数的方法
    }

    /**
     * 查询表中所有记录（使用自定义 SQL，自动映射到实体类）
     *
     * @param klass       实体类类型（需包含无参构造函数）
     * @param schemaTable 表结构元数据（需包含列定义）
     * @param sql         自定义查询 SQL（需包含参数占位符）
     * @param <T>         实体类型
     * @return 实体列表（无记录时返回空列表）
     * @throws DataException 若查询失败或实体类无效时抛出
     */
    public static <T> List<T> getAll(Class<T> klass, SchemaTable schemaTable, String sql) {
        // 参数校验
        Objects.requireNonNull(klass, "实体类不能为 null");
        Objects.requireNonNull(schemaTable, "表结构元数据不能为 null");
        Objects.requireNonNull(sql, "查询 SQL 不能为 null");

        try {
            List<Entity> entities = schemaTable.getDb().query(sql);
            return mapEntitiesToEntities(klass, schemaTable, entities);
        } catch (Exception e) {
            String errorMsg = StrUtil.format("查询所有记录失败：表={}，SQL={}，错误信息={}",
                    schemaTable.getRealTableName(), sql, e.getMessage());
            log.error(errorMsg, e);
            throw new DataException(errorMsg, e);
        }
    }

    // ========================== 分组记录查询 ==========================

    /**
     * 根据分组字段查询记录（自动映射到实体类）
     *
     * @param klass       实体类类型（需包含无参构造函数）
     * @param schemaTable 表结构元数据（需包含分组字段定义）
     * @param groupId     分组 ID 值
     * @param <T>         实体类型
     * @return 实体列表（无记录时返回空列表）
     * @throws DataException 若查询失败或实体类无效时抛出
     */
    public static <T> List<T> getGroup(Class<T> klass, SchemaTable schemaTable, String groupId) {
        // 参数校验
        Objects.requireNonNull(klass, "实体类不能为 null");
        Objects.requireNonNull(schemaTable, "表结构元数据不能为 null");
        Objects.requireNonNull(groupId, "分组 ID 不能为 null");

        String sql = MySqlExpert.genSelectByGroupId(schemaTable);
        try {
            List<Entity> entities = schemaTable.getDb().query(sql, groupId);
            return mapEntitiesToEntities(klass, schemaTable, entities);
        } catch (Exception e) {
            String errorMsg = StrUtil.format("查询分组记录失败：表={}，分组 ID={}，SQL={}，错误信息={}",
                    schemaTable.getRealTableName(), groupId, sql, e.getMessage());
            log.error(errorMsg, e);
            throw new DataException(errorMsg, e);
        }
    }

    // ========================== 私有工具方法 ==========================

    /**
     * 将第一条 Entity 记录映射为实体对象
     *
     * @param klass       实体类类型
     * @param schemaTable 表结构元数据
     * @param entity      数据库 Entity 记录
     * @param <T>         实体类型
     * @return 映射后的实体对象（映射失败时返回 null）
     */
    private static <T> T mapFirstEntityToEntity(Class<T> klass, SchemaTable schemaTable, Entity entity) {
        return mapEntityToEntity(klass, schemaTable, entity);
    }

    /**
     * 将 Entity 列表映射为实体对象列表
     *
     * @param klass       实体类类型
     * @param schemaTable 表结构元数据
     * @param entities    数据库 Entity 记录列表
     * @param <T>         实体类型
     * @return 实体对象列表（无记录时返回空列表）
     */
    private static <T> List<T> mapEntitiesToEntities(Class<T> klass, SchemaTable schemaTable, List<Entity> entities) {
        List<T> result = new ArrayList<>(entities.size());
        for (Entity entity : entities) {
            T obj = mapEntityToEntity(klass, schemaTable, entity);
            if (obj != null) {
                result.add(obj);
            }
        }
        return result;
    }

    /**
     * 将单个 Entity 记录映射为实体对象
     *
     * @param klass       实体类类型
     * @param schemaTable 表结构元数据
     * @param entity      数据库 Entity 记录
     * @param <T>         实体类型
     * @return 映射后的实体对象（映射失败时返回 null）
     */
    private static <T> T mapEntityToEntity(Class<T> klass, SchemaTable schemaTable, Entity entity) {
        try {
            T obj = ReflectUtil.newInstance(klass);
            if (obj == null) {
                log.warn("实体类实例化失败：类={}", klass.getName());
                return null;
            }
            for (SchemaColumn column : schemaTable.getColumns()) {
                String columnName = column.getName();
                Object dbValue = entity.get(columnName);
                column.setValue(obj, dbValue); // 使用 SchemaColumn 的类型转换能力
            }
            return obj;
        } catch (Exception e) {
            log.warn("实体映射失败：类={}，错误信息={}", klass.getName(), e.getMessage());
            return null;
        }
    }
}